﻿using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.Serialization;
using KC.Framework.Base;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage.Core.Util;

namespace KC.Model.Storage.Base
{
    [Serializable, DataContract(IsReference = true)]
    public class AzureTableEntity : Entity, ITableEntity
    {
        [NotMapped]
        [DataMember]
        public string PartitionKey { get; set; }
        [NotMapped]
        [DataMember]
        public string RowKey { get; set; }
        [NotMapped]
        [DataMember]
        public DateTimeOffset Timestamp { get; set; }
        [NotMapped]
        [DataMember]
        public string ETag { get; set; }

        [DataMember]
        public string Tenant { get; set; }

        protected AzureTableEntity()
            : this(string.Empty, string.Empty)
        {
        }


        protected AzureTableEntity(string patitionKey)
            : this(patitionKey, Guid.NewGuid().ToString())
        {

        }

        protected AzureTableEntity(string patitionKey, string rowKey)
        {
            PartitionKey = patitionKey;
            RowKey = rowKey;
            Timestamp = DateTime.UtcNow;
        }

        /// <summary>
        /// Deserializes the entity using the specified <see cref="IDictionary{TKey,TValue}"/> that maps property names to typed <see cref="EntityProperty"/> values. 
        /// </summary>
        /// <param name="properties">An <see cref="IDictionary{TKey,TValue}"/> object that maps property names to typed <see cref="EntityProperty"/> values.</param>
        /// <param name="operationContext">An <see cref="OperationContext"/> object that represents the context for the current operation.</param>
        public virtual void ReadEntity(IDictionary<string, EntityProperty> properties, OperationContext operationContext)
        {
            ReflectionRead(this, properties, operationContext);
        }

        /// <summary>
        /// Deserializes a custom entity instance using the specified <see cref="IDictionary{TKey,TValue}"/> of property names to <see cref="EntityProperty"/> data typed values. 
        /// </summary>
        /// <param name="entity">The custom entity instance being deserialized.</param>
        /// <param name="properties">An <see cref="IDictionary{TKey,TValue}"/> object that maps string property names to <see cref="EntityProperty"/> data values to deserialize and store in this table entity instance.</param>
        /// <param name="operationContext">An <see cref="OperationContext"/> object that represents the context for the current operation.</param>       
        public static void ReadUserObject(object entity, IDictionary<string, EntityProperty> properties, OperationContext operationContext)
        {
            ReflectionRead(entity, properties, operationContext);
        }

        /// <summary>
        /// Returns a custom entity instance which is recomposed using the specified <see cref="IDictionary{TKey,TValue}"/> of property names to <see cref="EntityProperty"/> data typed values.
        /// </summary>
        /// <param name="operationContext">An <see cref="OperationContext"/> object that represents the context for the current operation.</param>
        /// <typeparam name="TResult">The type of the recomposed object. This can be a simple object with a flat structure or a complex object with complex properties and multiple levels of object hierarchy.</typeparam>
        /// <param name="properties">An <see cref="IDictionary{TKey,TValue}"/> object that maps string property names to <see cref="EntityProperty"/> data values to deserialize and store in this table entity instance.</param>
        public static TResult ConvertBack<TResult>(IDictionary<string, EntityProperty> properties, OperationContext operationContext)
        {
            return EntityPropertyConverter.ConvertBack<TResult>(properties, operationContext);
        }

        /// <summary>
        /// Returns a custom entity instance which is recomposed using the specified <see cref="IDictionary{TKey,TValue}"/> of property names to <see cref="EntityProperty"/> data typed values.
        /// </summary>
        /// <typeparam name="TResult">The type of the recomposed object. This can be a simple object with a flat structure or a complex object with complex properties and multiple levels of object hierarchy.</typeparam>
        /// <param name="properties">An <see cref="IDictionary{TKey,TValue}"/> object that maps string property names to <see cref="EntityProperty"/> data values to deserialize and store in this table entity instance.</param>
        /// <param name="entityPropertyConverterOptions">A <see cref="EntityPropertyConverterOptions"/> object that specifies options for the entity property conversion.</param>
        /// <param name="operationContext">An <see cref="OperationContext"/> object that represents the context for the current operation.</param>
        public static TResult ConvertBack<TResult>(IDictionary<string, EntityProperty> properties, EntityPropertyConverterOptions entityPropertyConverterOptions, OperationContext operationContext)
        {
            return EntityPropertyConverter.ConvertBack<TResult>(properties, entityPropertyConverterOptions, operationContext);
        }

        [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1121:UseBuiltInTypeAlias", Justification = "Needed for object type checking.")]
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Reviewed")]
        private static void ReflectionRead(object entity, IDictionary<string, EntityProperty> properties, OperationContext operationContext)
        {
#if NETCORE
            IEnumerable<PropertyInfo> objectProperties = entity.GetType().GetTypeInfo().GetAllProperties();
#else
            IEnumerable<PropertyInfo> objectProperties = entity.GetType().GetProperties();
#endif
            foreach (PropertyInfo property in objectProperties)
            {
                if (ShouldSkipProperty(property, operationContext))
                {
                    continue;
                }

                // only proceed with properties that have a corresponding entry in the dictionary
                if (!properties.ContainsKey(property.Name))
                {
                    continue;
                }

                EntityProperty entityProperty = properties[property.Name];

                if (entityProperty == null)
                {
                    property.SetValue(entity, null, null);
                }
                else
                {
                    switch (entityProperty.PropertyType)
                    {
                        case EdmType.String:
                            if (property.PropertyType == typeof(double) || property.PropertyType == typeof(double?))
                            {
                                property.SetValue(entity, entityProperty.DoubleValue, null);
                            }
                            else if (property.PropertyType != typeof(string))
                            {
                                continue;
                            }

                            property.SetValue(entity, entityProperty.StringValue, null);
                            break;
                        case EdmType.Binary:
                            if (property.PropertyType != typeof(byte[]))
                            {
                                continue;
                            }

                            property.SetValue(entity, entityProperty.BinaryValue, null);
                            break;
                        case EdmType.Boolean:
                            if (property.PropertyType != typeof(bool) && property.PropertyType != typeof(bool?))
                            {
                                continue;
                            }

                            property.SetValue(entity, entityProperty.BooleanValue, null);
                            break;
                        case EdmType.DateTime:
                            if (property.PropertyType == typeof(DateTime))
                            {
                                property.SetValue(entity, entityProperty.DateTimeOffsetValue.Value.UtcDateTime, null);
                            }
                            else if (property.PropertyType == typeof(DateTime?))
                            {
                                property.SetValue(entity, entityProperty.DateTimeOffsetValue.HasValue ? entityProperty.DateTimeOffsetValue.Value.UtcDateTime : (DateTime?)null, null);
                            }
                            else if (property.PropertyType == typeof(DateTimeOffset))
                            {
                                property.SetValue(entity, entityProperty.DateTimeOffsetValue.Value, null);
                            }
                            else if (property.PropertyType == typeof(DateTimeOffset?))
                            {
                                property.SetValue(entity, entityProperty.DateTimeOffsetValue, null);
                            }

                            break;
                        case EdmType.Double:
                            if (property.PropertyType != typeof(double) && property.PropertyType != typeof(double?))
                            {
                                continue;
                            }

                            property.SetValue(entity, entityProperty.DoubleValue, null);
                            break;
                        case EdmType.Guid:
                            if (property.PropertyType != typeof(Guid) && property.PropertyType != typeof(Guid?))
                            {
                                continue;
                            }

                            property.SetValue(entity, entityProperty.GuidValue, null);
                            break;
                        case EdmType.Int32:
                            if (property.PropertyType != typeof(int) && property.PropertyType != typeof(int?))
                            {
                                continue;
                            }

                            property.SetValue(entity, entityProperty.Int32Value, null);
                            break;
                        case EdmType.Int64:
                            if (property.PropertyType != typeof(long) && property.PropertyType != typeof(long?))
                            {
                                continue;
                            }

                            property.SetValue(entity, entityProperty.Int64Value, null);
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// Serializes the <see cref="IDictionary{TKey,TValue}"/> of property names mapped to <see cref="EntityProperty"/> data values from this <see cref="TableEntity"/> instance.
        /// </summary>
        /// <param name="operationContext">An <see cref="OperationContext"/> object that represents the context for the current operation.</param>
        /// <returns>An <see cref="IDictionary{TKey,TValue}"/> object that maps string property names to <see cref="EntityProperty"/> typed values created by serializing this table entity instance.</returns>
        public virtual IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
        {
            return ReflectionWrite(this, operationContext);
        }

        /// <summary>
        /// Create a <see cref="IDictionary{TKey,TValue}"/> of <see cref="EntityProperty"/> objects for all the properties of the specified entity object.
        /// </summary>
        /// <param name="entity">The entity object to serialize.</param>
        /// <param name="operationContext">An <see cref="OperationContext"/> object that represents the context for the current operation.</param>
        /// <returns>An <see cref="IDictionary{TKey,TValue}"/> of <see cref="EntityProperty"/> objects for all the properties of the specified entity object.</returns>
        public static IDictionary<string, EntityProperty> WriteUserObject(object entity, OperationContext operationContext)
        {
            return ReflectionWrite(entity, operationContext);
        }

        /// <summary>
        /// Flattens the entity and creates a <see cref="IDictionary{TKey,TValue}"/> of <see cref="EntityProperty"/> objects for all properties of the specified entity object.
        /// </summary>
        /// <param name="entity">The entity object to serialize.</param>
        /// <param name="operationContext">An <see cref="OperationContext"/> object that represents the context for the current operation.</param>
        /// <returns>An <see cref="IDictionary{TKey,TValue}"/> of <see cref="EntityProperty"/> objects for all the properties of the specified entity object.</returns>
        /// <remarks>The entity type can be a simple object with a flat structure or a complex object with complex properties and multiple levels of object hierarchy.
        /// Generic ConvertBack method can recompose the original entity using the return value of this method.</remarks>
        public static IDictionary<string, EntityProperty> Flatten(object entity, OperationContext operationContext)
        {
            return EntityPropertyConverter.Flatten(entity, operationContext);
        }

        /// <summary>
        /// Flattens the entity and creates a <see cref="IDictionary{TKey,TValue}"/> of <see cref="EntityProperty"/> objects for all properties of the specified entity object.
        /// </summary>
        /// <param name="entity">The entity object to serialize.</param>
        /// <param name="entityPropertyConverterOptions">A <see cref="EntityPropertyConverterOptions"/> object that specifies options for the entity property conversion.</param>
        /// <param name="operationContext">An <see cref="OperationContext"/> object that represents the context for the current operation.</param>
        /// <returns>An <see cref="IDictionary{TKey,TValue}"/> of <see cref="EntityProperty"/> objects for all the properties of the specified entity object.</returns>
        /// <remarks>The entity type can be a simple object with a flat structure or a complex object with complex properties and multiple levels of object hierarchy.
        /// Generic ConvertBack method can recompose the original entity using the return value of this method.</remarks>
        public static IDictionary<string, EntityProperty> Flatten(object entity, EntityPropertyConverterOptions entityPropertyConverterOptions, OperationContext operationContext)
        {
            return EntityPropertyConverter.Flatten(entity, entityPropertyConverterOptions, operationContext);
        }

        private static IDictionary<string, EntityProperty> ReflectionWrite(object entity, OperationContext operationContext)
        {
            Dictionary<string, EntityProperty> retVals = new Dictionary<string, EntityProperty>();

#if NETCORE
            IEnumerable<PropertyInfo> objectProperties = entity.GetType().GetTypeInfo().GetAllProperties();
#else
            IEnumerable<PropertyInfo> objectProperties = entity.GetType().GetProperties();
#endif

            foreach (PropertyInfo property in objectProperties)
            {
                if (ShouldSkipProperty(property, operationContext))
                {
                    continue;
                }

                EntityProperty newProperty = CreateEntityPropertyFromObject(property.GetValue(entity, null), property.PropertyType);


                // property will be null if unknown type
                if (newProperty != null)
                {
                    retVals.Add(property.Name, newProperty);
                }
            }

            return retVals;
        }


        #region Static Helpers
        /// <summary>
        /// Determines if the given property should be skipped based on its name, if it exposes a public getter and setter, and if the IgnoreAttribute is not defined.
        /// </summary>
        /// <param name="property">The PropertyInfo of the property to check</param>
        /// <param name="operationContext">An <see cref="OperationContext"/> object that represents the context for the current operation.</param>
        /// <returns>True if the property should be skipped, false otherwise. </returns>
        internal static bool ShouldSkipProperty(PropertyInfo property, OperationContext operationContext)
        {
            // reserved properties
            string propName = property.Name;
            if (propName == "PartitionKey" ||
                propName == "RowKey" ||
                propName == "Timestamp" ||
                propName == "Etag")
            {
                return true;
            }

#if WINDOWS_RT || NETCORE
            MethodInfo setter = property.SetMethod;
            MethodInfo getter = property.GetMethod;
#else
            MethodInfo setter = property.GetSetMethod();
            MethodInfo getter = property.GetGetMethod();
#endif
            // Enforce public getter / setter
            if (setter == null || !setter.IsPublic || getter == null || !getter.IsPublic)
            {
                return true;
            }

            // Skip static properties
            if (setter.IsStatic)
            {
                return true;
            }

            // properties with [IgnoreAttribute]
#if WINDOWS_RT || NETCORE 
            if (property.GetCustomAttribute(typeof(IgnorePropertyAttribute)) != null)
#else
            if (Attribute.IsDefined(property, typeof(IgnorePropertyAttribute)))
#endif
            {
                return true;
            }

            return false;
        }

        internal static EntityProperty CreateEntityPropertyFromObject(object value, Type type)
        {
            if (type == typeof(string))
            {
                return new EntityProperty((string)value);
            }
            else if (type == typeof(byte[]))
            {
                return new EntityProperty((byte[])value);
            }
            else if (type == typeof(bool))
            {
                return new EntityProperty((bool)value);
            }
            else if (type == typeof(bool?))
            {
                return new EntityProperty((bool?)value);
            }
            else if (type == typeof(DateTime))
            {
                return new EntityProperty((DateTime)value);
            }
            else if (type == typeof(DateTime?))
            {
                return new EntityProperty((DateTime?)value);
            }
            else if (type == typeof(DateTimeOffset))
            {
                return new EntityProperty((DateTimeOffset)value);
            }
            else if (type == typeof(DateTimeOffset?))
            {
                return new EntityProperty((DateTimeOffset?)value);
            }
            else if (type == typeof(double))
            {
                return new EntityProperty((double)value);
            }
            else if (type == typeof(double?))
            {
                return new EntityProperty((double?)value);
            }
            else if (type == typeof(Guid?))
            {
                return new EntityProperty((Guid?)value);
            }
            else if (type == typeof(Guid))
            {
                return new EntityProperty((Guid)value);
            }
            else if (type == typeof(int))
            {
                return new EntityProperty((int)value);
            }
            else if (type == typeof(int?))
            {
                return new EntityProperty((int?)value);
            }
            else if (type == typeof(long))
            {
                return new EntityProperty((long)value);
            }
            else if (type == typeof(long?))
            {
                return new EntityProperty((long?)value);
            }
            else
            {
                return null;
            }
        }

        #endregion



    }
}